<?php

namespace Yjius\fileshandler\local;


//谨慎使用,稳定性安全性未验证
//大文件分片上传类
use Yjius\libraries\CFileCache;

class UploadHelper
{
    //分片临时文件目录
    private $tmpDir = __DIR__ . '/../../../examples/runtime/tmp';

    //文件最终保存目录
    private $saveDir = __DIR__ . '/../../../examples/runtime/media';

    private static $pathDir = '/uploads/files';

    private static $mysql = null;

    public function __construct()
    {

    }

    public $fileInfo = [
        'identifier' => '',  //文件的唯一标识
        'chunkNumber' => 1, //当前是第几个分片
        'totalChunks' => 1,  //总分片数
        'filename' => '',  //文件名称
        'totalSize' => 0  //文件总大小
    ];

    //检测断点和md5
    public function checkFile()
    {
        $identifier = $this->fileInfo['identifier'];
        $filePath = $this->tmpDir . DIRECTORY_SEPARATOR . $identifier; //临时分片文件路径
        $totalChunks = $this->fileInfo['totalChunks'];
        //检测文件md5是否已经存在
        $rs = $this->checkMd5($identifier, $this->fileInfo['totalSize']);
        if ($rs['isExist'] === true) {
            //文件已存在
            return $rs;
        }

        //检查分片是否存在
        $chunkExists = [];
        for ($index = 1; $index <= $totalChunks; $index++) {
            if (file_exists("{$filePath}_{$index}")) {
                array_push($chunkExists, $index);
            }
        }
        if (count($chunkExists) == $totalChunks) { //全部分片存在，则直接合成
            $this->merge();
        } else {
            $res['uploaded'] = $chunkExists;
            return $res;
        }
    }

    //上传分片
    public function upload()
    {
        if (!empty($_FILES)) {
            $in = @fopen($_FILES["file"]["tmp_name"], "rb");
            if (!$in = @fopen($_FILES["file"]["tmp_name"], "rb")) {
                throw new \Exception("打开临时文件失败");
            }
        } else {
            if (!$in = @fopen("php://input", "rb")) {
                throw new \Exception("打开输入流失败");
            }
        }
        $filename = $ext = '';
        if ($this->fileInfo['totalChunks'] === 1) {
            //如果只有1片，则不需要合并，直接将临时文件转存到保存目录下
            $ext = strtolower(pathinfo($this->fileInfo['filename'], PATHINFO_EXTENSION));
            $filename = $this->fileInfo['identifier'] . '.' . $ext;

            $saveDir = $this->saveDir . DIRECTORY_SEPARATOR . date('Ymd');
            if (!is_dir($saveDir)) {
                @mkdir($saveDir);
            }

            $uploadPath = $saveDir . DIRECTORY_SEPARATOR . $filename;
            $res['merge'] = false;
        } else { //需要合并
            $filePath = $this->tmpDir . DIRECTORY_SEPARATOR . $this->fileInfo['identifier']; //临时分片文件路径
            $uploadPath = $filePath . '_' . $this->fileInfo['chunkNumber']; //临时分片文件名
            $res['merge'] = true;
        }
        if (!$out = @fopen($uploadPath, "wb")) {
            throw new \Exception("文件不可写");
        }
        while ($buff = fread($in, 4096)) {
            fwrite($out, $buff);
        }
        @fclose($in);
        @fclose($out);

        $res['end'] = 0;
        if ($this->fileInfo['totalChunks'] === 1) {
            $res['data'] = [
                'filename' => $filename,
                'filesize' => $this->fileInfo['totalSize'],
                'md5' => $this->fileInfo['identifier'],
                'filepath' => self::$pathDir . '/' . date('Ymd') . '/' . $filename,
                'ext' => $ext
            ];
            $res['end'] = 1;
        }
        $res['code'] = 0;
        return $res;
    }

    public function merge()
    {
        $filePath = $this->tmpDir . DIRECTORY_SEPARATOR . $this->fileInfo['identifier'];

        $totalChunks = $this->fileInfo['totalChunks']; //总分片数
        //$filename = $this->fileInfo['filename']; //文件名
        $ext = strtolower(pathinfo($this->fileInfo['filename'], PATHINFO_EXTENSION));
        $filename = $this->fileInfo['identifier'] . '.' . $ext;

        $done = true;
        //检查所有分片是否都存在
        for ($index = 1; $index <= $totalChunks; $index++) {
            if (!file_exists("{$filePath}_{$index}")) {
                $done = false;
                break;
            }
        }
        if ($done === false) {
            throw new \Exception("分片信息错误");
        }
        //如果所有文件分片都上传完毕，开始合并
        $timeStart = $this->getmicrotime(); //合并开始时间
        $saveDir = $this->saveDir . DIRECTORY_SEPARATOR . date('Ymd');
        if (!is_dir($saveDir)) {
            @mkdir($saveDir);
        }

        $uploadPath = $saveDir . DIRECTORY_SEPARATOR . $filename;

        if (!$out = @fopen($uploadPath, "wb")) {
            throw new \Exception("文件不可写");
        }
        if (flock($out, LOCK_EX)) { // 进行排他型锁定
            for ($index = 1; $index <= $totalChunks; $index++) {
                if (!$in = @fopen("{$filePath}_{$index}", "rb")) {
                    break;
                }
                while ($buff = fread($in, 4096)) {
                    fwrite($out, $buff);
                }
                @fclose($in);
                @unlink("{$filePath}_{$index}"); //删除分片
            }

            flock($out, LOCK_UN); // 释放锁定
        }
        @fclose($out);
        $timeEnd = $this->getmicrotime(); //合并完成时间

        $res['code'] = 0;
        $res['time'] = $timeEnd - $timeStart; //合并总耗时

        $res['data'] = [
            'filename' => $filename,
            'filesize' => $this->fileInfo['totalSize'],
            'md5' => $this->fileInfo['identifier'],
            'filepath' => self::$pathDir . '/' . date('Ymd') . '/' . $filename,
            'ext' => $ext
        ];
        $res['end'] = 1;
        //写入缓存
        $cache = new CFileCache();
        $cacheKey = 'upload_' . $res['data']['md5'] . '_' . $res['data']['filesize'];
        $cache->set($cacheKey,json_encode($res['data']));
        return $res;
    }

    //检测md5表是否已存在该文件
    private function checkMd5($md5, $filesize)
    {
        $res['isExist'] = false;
        $cache = new CFileCache();
        $cacheKey = 'upload_' . $md5 . '_' . $filesize;
        if ($cache->exists($cacheKey)) {
            $row = $cache->get($cacheKey);
            $row = json_decode($row, true);
            if (!empty($row['filepath'])) {
                $res['isExist'] = true;
                $res['filepath'] = $row['filepath'];
            } else {
                $res['isExist'] = false;
            }
        }
        return $res;
    }

    //计算时间
    private function getmicrotime()
    {
        list($usec, $sec) = explode(" ", microtime());
        return ((float)$usec + (float)$sec);
    }

    //返回提示消息
    private function message($code, $msg)
    {
        $res = [
            'code' => $code,
            'message' => $msg
        ];
        return $res;
    }

    /**
     * @return string
     */
    public function getTmpDir()
    {
        return $this->tmpDir;
    }

    /**
     * @param string $tmpDir
     */
    public function setTmpDir($tmpDir)
    {
        $this->tmpDir = $tmpDir;
    }

    /**
     * @return string
     */
    public function getSaveDir()
    {
        return $this->saveDir;
    }

    /**
     * @param string $saveDir
     */
    public function setSaveDir($saveDir)
    {
        $this->saveDir = $saveDir;
    }
}


